//-----------------------------------------------------------------------------
// Filename: SIPTCPChannel.cs
//
// Description: SIP transport for TCP.
// 
// History:
// 19 Apr 2008	Aaron Clauson	Created.
//
// License: 
// This software is licensed under the BSD License http://www.opensource.org/licenses/bsd-license.php
//
// Copyright (c) 2006 Aaron Clauson (aaronc@blueface.ie), Blue Face Ltd, Dublin, Ireland (www.blueface.ie)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that 
// the following conditions are met:
//
// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 
// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 
// disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Blue Face Ltd. 
// nor the names of its contributors may be used to endorse or promote products derived from this software without specific 
// prior written permission. 
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
// BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
// OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
// POSSIBILITY OF SUCH DAMAGE.
//-----------------------------------------------------------------------------

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BlueFace.Sys;
using BlueFace.Sys.Net;
using BlueFace.VoIP.Net;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace BlueFace.VoIP.Net.SIP
{
    public delegate void SIPTCPMessageReceivedDelegate(IPEndPoint remoteEndPoint, byte[] buffer);
    public delegate void SIPTCPSocketDisconnectedDelegate(IPEndPoint remoteEndPoint);

    public enum SIPTCPConnectionsEnum
    {
        Listener = 1,   // Indicates the connection was initiated by the remote client to a local server socket.
        Caller = 2,     // Indicated the connection was initiated locally to a remote server socket.
    }
    
    public class SIPTCPConnection
    {
        private ILog logger = AppSpecificState.logger;
        
        public static int MaxSIPTCPMessageSize = SIPConstants.SIP_MAXIMUM_LENGTH;
        private static string m_sipTCPMessageDelimiter = SIPConstants.CRLF + SIPConstants.CRLF;
        private static char[] delimiterChars = new char[] { ':' };

        public Socket TCPSocket;
        public IPEndPoint RemoteEndPoint;
        public SIPTCPConnectionsEnum ConnectionType;
        public byte[] SocketBuffer;

        private int m_bufferEndIndex = 0;

        public event SIPTCPMessageReceivedDelegate SIPTCPMessageReceived;
        public event SIPTCPSocketDisconnectedDelegate SIPTCPSocketDisconnected;

        public SIPTCPConnection(Socket tcpSocket, IPEndPoint remoteEndPoint, SIPTCPConnectionsEnum connectionType)
        {
            TCPSocket = tcpSocket;
            RemoteEndPoint = remoteEndPoint;
            ConnectionType = connectionType;
            SocketBuffer = new byte[MaxSIPTCPMessageSize];
        }

        public void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                int bytesRead = TCPSocket.EndReceive(ar);

                logger.Debug(bytesRead + " bytes read from " + RemoteEndPoint + ".");

                if (bytesRead > 0)
                {
                    m_bufferEndIndex += bytesRead;
                    int endMessageIndex = ByteBufferInfo.GetStringPosition(SocketBuffer, m_sipTCPMessageDelimiter, null);
                    if (endMessageIndex != -1)
                    {
                        //logger.Debug("SIP message delimiter found in buffer. SIP Message:\n" + Encoding.UTF8.GetString(SocketBuffer, 0, endMessageIndex));

                        int contentLength = 0;

                        string[] headers = SIPHeader.SplitHeaders(Encoding.UTF8.GetString(SocketBuffer, 0, endMessageIndex));
                        foreach(string header in headers)
                        {
					        // If the first character of a line is whitespace it's a contiuation of the previous header and not the Content-Lenght header which won't be spread over multiple lines.
                            if (header.StartsWith(" "))
					        {
						        continue;
					        }
					        else
					        {
                                string[] headerParts = header.Trim().Split(delimiterChars, 2);

						        if(headerParts == null || headerParts.Length <2)
						        {
							        // Invalid SIP header, ignoring.
							        continue;
						        }

						        string headerName = headerParts[0].Trim().ToLower();
                                if (headerName == SIPHeaders.SIP_COMPACTHEADER_CONTENTLENGTH.ToLower() ||
                                    headerName == SIPHeaders.SIP_HEADER_CONTENTLENGTH.ToLower())
                                {
                                    string headerValue = headerParts[1].Trim();
                                    if (!Int32.TryParse(headerValue, out contentLength))
                                    {
                                        logger.Warn("The content length could not be parsed from " + headerValue + " in the SIPTCPChannel. buffer now invalid and being purged.");
                                    }
                                    //else
                                    //{
                                    //    logger.Debug("Content-Length=" + contentLength + ".");
                                    //}
                                }
                            }
                        }

                        if (SocketBuffer.Length >= endMessageIndex + m_sipTCPMessageDelimiter.Length + contentLength)
                        {
                            byte[] sipMsgBuffer = new byte[endMessageIndex + m_sipTCPMessageDelimiter.Length + contentLength];
                            Buffer.BlockCopy(SocketBuffer, 0, sipMsgBuffer, 0, endMessageIndex + m_sipTCPMessageDelimiter.Length + contentLength);
                            endMessageIndex = endMessageIndex - sipMsgBuffer.Length + m_sipTCPMessageDelimiter.Length + contentLength;

                            logger.Debug("SIP TCP message detected length=" + sipMsgBuffer.Length + ", buffer end index=" + endMessageIndex + ".");

                            if (SIPTCPMessageReceived != null)
                            {
                                SIPTCPMessageReceived(RemoteEndPoint, sipMsgBuffer);
                            }
                        }
                        else
                        {
                            logger.Warn("The Body of a SIP TCP message has not yet been received.");
                        }
                    }

                    SocketBuffer = new byte[MaxSIPTCPMessageSize];
                    TCPSocket.BeginReceive(SocketBuffer, 0, MaxSIPTCPMessageSize, SocketFlags.None, new AsyncCallback(ReceiveCallback), null);
                }
                else
                {
                    logger.Debug("SIP TCP socket to " + RemoteEndPoint + " was diconnected, closing.");
                    
                    TCPSocket.Close();

                    if (SIPTCPSocketDisconnected != null)
                    {
                        SIPTCPSocketDisconnected(RemoteEndPoint);
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception ReceiveCallback. " + excp);
            }
        }
    }
    
    public class SIPTCPChannel : SIPChannel
	{
        private const int MAX_TCP_CONNECTIONS = 10000;   // Maximum number of connections for the TCP listener.
        
        private ILog logger = AppSpecificState.logger;

		private Socket m_sipConn = null;
		private bool m_closed = false;
        private Dictionary<string, SIPTCPConnection> m_listenerConnections = new Dictionary<string, SIPTCPConnection>();  // Connected sockets established from the remote end with the local TCP listener.
        private Dictionary<string, SIPTCPConnection> m_callerConnections = new Dictionary<string, SIPTCPConnection>();  // Connected sockets established by this process to remote servers.

		public SIPTCPChannel(IPEndPoint endPoint)
		{	
			try
			{
                m_localEndPoint = endPoint;
                m_isReliable = true;
                m_sipProtocol = SIPProtocolsEnum.TCP;
                m_sipConn = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                m_sipConn.Bind(endPoint);

                Thread listenThread = new Thread(new ThreadStart(AcceptConnections));
                listenThread.Start();

				logger.Info("SIP TCP Channel listener created " + endPoint + ".");
			}
			catch(Exception excp)
			{
				logger.Error("Exception SIPTCPChannel (ctor). " + excp.Message);
				throw excp;
			}
		}

		public void Dispose(bool disposing)
		{
			try
			{
				this.Close();
			}
			catch(Exception excp)
			{
				logger.Error("Exception Disposing SIPTCPChannel. " + excp.Message);
			}
		}
				
		private void AcceptConnections()
		{
			try
			{
                m_sipConn.Listen(MAX_TCP_CONNECTIONS);
               
                logger.Debug("SIPTCPChannel socket on " + m_localEndPoint + " listening started.");

				while(!m_closed)
				{
                    Socket clientSocket = m_sipConn.Accept();
                    IPEndPoint remoteEndPoint = (IPEndPoint)clientSocket.RemoteEndPoint;
                    logger.Debug("SIP TCP Channel connection accepted from " + remoteEndPoint + ".");

                    lock(m_listenerConnections)
                    {
                        if (m_listenerConnections.ContainsKey(remoteEndPoint.ToString()))
                        {
                            // A new connection has arrived on a socket that's already in the list of connected clients. Close the current one and accept the new one.
                            logger.Warn("A new connection was received from " + remoteEndPoint + " which is already connected.");
                            m_listenerConnections.Remove(remoteEndPoint.ToString());
                        }
                        
                        SIPTCPConnection sipTCPClient = new SIPTCPConnection(clientSocket, remoteEndPoint, SIPTCPConnectionsEnum.Listener);
                        m_listenerConnections.Add(remoteEndPoint.ToString(), sipTCPClient);
                        sipTCPClient.SIPTCPSocketDisconnected += new SIPTCPSocketDisconnectedDelegate(SIPTCPSocketDisconnected);
                        sipTCPClient.SIPTCPMessageReceived += new SIPTCPMessageReceivedDelegate(SIPTCPMessageReceived);
                        clientSocket.BeginReceive(sipTCPClient.SocketBuffer, 0, SIPTCPConnection.MaxSIPTCPMessageSize, SocketFlags.None, new AsyncCallback(sipTCPClient.ReceiveCallback), null);
                    }
 				}

                logger.Debug("SIPTCPChannel socket on " + m_localEndPoint + " listening halted.");
			}
			catch(Exception excp)
			{
				logger.Error("Exception SIPTCPChannel Listen. " + excp.Message);
				//throw excp;
			}
		}

        private void SIPTCPSocketDisconnected(IPEndPoint remoteEndPoint)
        {
            try
            {
                lock (m_listenerConnections)
                {
                    if (m_listenerConnections.ContainsKey(remoteEndPoint.ToString()))
                    {
                        m_listenerConnections.Remove(remoteEndPoint.ToString());
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPTCPClientDisconnected. " + excp);
            }
        }

        private void SIPTCPMessageReceived(IPEndPoint remoteEndPoint, byte[] buffer)
        {
            if (SIPMessageReceived != null)
            {
                SIPMessageReceived(this, remoteEndPoint, buffer);
            }
        }

        public override void Send(IPEndPoint destinationEndPoint, string message)
        {
            byte[] messageBuffer = Encoding.UTF8.GetBytes(message);
            Send(destinationEndPoint, messageBuffer);
        }

        public override void Send(IPEndPoint destinationEndPoint, byte[] buffer)
		{			
			try
			{
                if (destinationEndPoint == null)
                {
                    logger.Error("An empty destination was specified to Send in SIPTCPChannel.");
                }
                else
                {
                    // Lookup a client socket that is connected to the destination.
                    //m_sipConn(buffer, buffer.Length, destinationEndPoint);
                    if (m_listenerConnections.ContainsKey(destinationEndPoint.ToString()))
                    {
                        SIPTCPConnection sipTCPClient = m_listenerConnections[destinationEndPoint.ToString()];
                        sipTCPClient.TCPSocket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(EndSend), sipTCPClient);
                    }
                    else if (m_callerConnections.ContainsKey(destinationEndPoint.ToString()))
                    {
                        SIPTCPConnection sipTCPClient = m_callerConnections[destinationEndPoint.ToString()];
                        sipTCPClient.TCPSocket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(EndSend), sipTCPClient);
                    }
                    else
                    {
                        logger.Warn("The SIPTCPChannel was asked to send to a non-connected socket. Use Send with re-connect if a connection should be attempted.");
                    }
                }
			}
			catch(ObjectDisposedException)
			{
				logger.Warn("The SIPTCPchannel was not accessible when attempting to send a message to, " + destinationEndPoint + ".");
			}
			catch(Exception excp)
			{
				logger.Error("Exception (" + excp.GetType().ToString() + ") SIPTCPChannel Send (sendto=>" + destinationEndPoint + "). " + excp.Message);
				throw excp;
			}
		}

        public override void Send(IPEndPoint dstEndPoint, byte[] buffer, bool reconnect)
        {
            try
            {
                if (dstEndPoint == null)
                {
                    logger.Error("An empty destination was specified to Send in SIPTCPChannel.");
                }
                else
                {
                    // Lookup a client socket that is connected to the destination.
                    //m_sipConn(buffer, buffer.Length, destinationEndPoint);
                    if (m_listenerConnections.ContainsKey(dstEndPoint.ToString()) || m_callerConnections.ContainsKey(dstEndPoint.ToString()))
                    {
                        Send(dstEndPoint, buffer);
                    }
                    else if (reconnect)
                    {
                        Socket callerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                        callerSocket.BeginConnect(dstEndPoint, EndConnect, new object[] { callerSocket, dstEndPoint, buffer});
                    }
                    else
                    {
                        logger.Debug("The SIPTCPChannel did not send to due to no active connection available.");
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception (" + excp.GetType().ToString() + ") SIPTCPChannel Send (sendto=>" + dstEndPoint + "). " + excp.Message);
                throw excp;
            }
        }

        private void EndSend(IAsyncResult ar)
        {
            try
            {
                SIPTCPConnection sipTCPConnection = (SIPTCPConnection)ar.AsyncState;
                int bytesSent = sipTCPConnection.TCPSocket.EndSend(ar);

                logger.Debug(bytesSent + " successfully sent to TCP " + sipTCPConnection.TCPSocket.RemoteEndPoint + ".");
            }
            catch (Exception excp)
            {
                logger.Error("Exception EndSend. " + excp);
            }
        }

        private void EndConnect(IAsyncResult ar)
        {
            try
            {
                object[] stateObj = (object[])ar.AsyncState;
                Socket callerSocket = (Socket)stateObj[0];
                IPEndPoint dstEndPoint = (IPEndPoint)stateObj[1];
                byte[] buffer = (byte[])stateObj[2];

                callerSocket.EndConnect(ar);

                if (callerSocket != null && callerSocket.Connected)
                {
                     lock(m_callerConnections)
                    {
                        if(m_callerConnections.ContainsKey(dstEndPoint.ToString()))
                        {
                            // A new connection was established to a socket that's already in the list of connected clients. Close the current one and keep the new one.
                            logger.Warn("A new connection was establised to an already connected socket " + dstEndPoint + ".");
                            m_callerConnections.Remove(dstEndPoint.ToString());
                        }

                        SIPTCPConnection callerConnection = new SIPTCPConnection(callerSocket, dstEndPoint, SIPTCPConnectionsEnum.Caller);
                        m_callerConnections.Add(dstEndPoint.ToString(), callerConnection);
                        callerConnection.SIPTCPSocketDisconnected += new SIPTCPSocketDisconnectedDelegate(SIPTCPSocketDisconnected);
                        callerConnection.SIPTCPMessageReceived += new SIPTCPMessageReceivedDelegate(SIPTCPMessageReceived);

                        callerSocket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, EndSend, null);
                     }
                }
                else
                {
                    logger.Warn("Could not establish TCP connection to " + dstEndPoint + ".");
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception SIPTCPChannel EndConnect. " + excp);
            }
        }

        public override void Close()
		{
			try
			{
				logger.Debug("Closing SIP TCP Channel " + SIPChannelEndPoint + ".");
				
				m_closed = true;
                m_sipConn.Shutdown(SocketShutdown.Both);
                m_sipConn.Close();
			}
			catch(Exception excp)
			{
				logger.Warn("Exception SIPTCPChannel Close. " +excp.Message);
			}
		}

		#region Unit testing.

		#if UNITTEST
	
		[TestFixture]
		public class SIPRequestUnitTest
		{
			[TestFixtureSetUp]
			public void Init()
			{
				
			}

			[TestFixtureTearDown]
			public void Dispose()
			{			
				
			}

			[Test]
			public void SampleTest()
			{
				Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				Assert.IsTrue(true, "True was false.");
			}
		}

		#endif

		#endregion
	}
}
